---
title: MemFree 构建故事 4 -- Bun 流响应用于 gpt-4o 图像输入
description: Bun 流响应用于 gpt-4o 图像输入
image: /images/blog/blog-post-3.jpg
date: '2024-06-15'
---

MemFree 的后端接口是基于 Bun 构建的。使用 Bun 建立一个 Web API 非常简单。本文介绍如何使用 Bun 构建一个接受图像输入并以流格式返回结果的 gpt-4o Web API。

## 1 Bun 流返回结果示例

在以下示例中，使用 openai 库，模型是 gpt-3.5。

```ts
import OpenAI from 'openai';

const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY!,
});

type Message = OpenAI.Chat.Completions.ChatCompletionMessageParam;

export async function OpenAIStream(userInput: string) {
    const messages: Message[] = [
        {
            role: 'system',
            content: '你是一个乐于助人的助手，总是称呼用户为朋友，并且大量使用表情符号。',
        },
    ];
    messages.push({
        role: 'user',
        content: userInput,
    });

    const res = await openai.chat.completions.create({
        model: 'gpt-3.5-turbo',
        messages: messages,
        max_tokens: 1024,
        stream: true,
        temperature: 0.3,
    });

    return new ReadableStream({
        async start(controller) {
            for await (const chunk of res) {
                if (chunk.choices[0].delta.content) {
                    controller.enqueue(chunk.choices[0].delta.content);
                } else if (chunk.choices[0].finish_reason != null) {
                    controller.close();
                    break;
                }
            }
        },
    });
}
```

## 2 Bun 本地图像用于 gpt-4o 输入

以下代码将读取本地目录中的图像文件，将其转换为 base64 格式，作为 gpt-4o 的输入，并以流模式返回结果。

```ts
import OpenAI from 'openai';

import { readFile, readdir } from 'node:fs/promises';
import { dirname, join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';

const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY!,
});

async function readImageFile(filePath: string) {
    const baseDir = dirname(fileURLToPath(import.meta.url));
    return await readFile(join(baseDir, filePath));
}

export async function chatImage() {
    const inputsDir = join(dirname(fileURLToPath(import.meta.url)), '/input');

    const filePaths = await readdir(inputsDir).then((fileNames) => fileNames.map((fileName) => `/${relative(dirname(inputsDir), join(inputsDir, fileName))}`));

    const base64Image = await readImageFile(filePaths[0]).then((file) => file.toString('base64'));

    const res = await openai.chat.completions.create({
        model: 'gpt-4o',
        messages: [
            {
                role: 'system',
                content: `请提取图像中的文本`,
            },
            {
                role: 'user',
                content: [
                    {
                        type: 'image_url',
                        image_url: {
                            url: `data:image/jpeg;base64,${base64Image}`,
                        },
                    },
                ],
            },
        ],
        response_format: { type: 'json_object' },
        temperature: 0.3,
        stream: true,
    });

    return new ReadableStream({
        async start(controller) {
            for await (const chunk of res) {
                if (chunk.choices[0].delta.content) {
                    console.log('chunk delta ', chunk.choices[0].delta.content);
                    controller.enqueue(chunk.choices[0].delta.content);
                } else if (chunk.choices[0].finish_reason != null) {
                    controller.close();
                    break;
                }
            }
        },
    });
}
```

## 3 Bun 处理带图像的 FormData

以下代码处理浏览器客户端上传的图像。它也首先转换为 base64 格式，然后用作 gpt-4o 的输入。

```ts
import OpenAI from 'openai';

const openai = new OpenAI({
    apiKey: process.env.OPENAI_API_KEY!,
});

export async function chatImageIdea(req: Request) {
    const formData = await req.formData();
    const number = formData.get('number');
    const question = formData.get('question');
    const language = formData.get('language') as string;
    const file = formData.get('image') as File;
    const ab = await file.arrayBuffer();
    const baseImage = Buffer.from(ab).toString('base64');

    const prompt = `xxxx`;

    const res = await openai.chat.completions.create({
        model: 'gpt-4o',
        messages: [
            {
                role: 'system',
                content: prompt,
            },
            {
                role: 'user',
                content: [
                    {
                        type: 'image_url',
                        image_url: {
                            url: `data:image/jpeg;base64,${baseImage}`,
                        },
                    },
                ],
            },
        ],
        temperature: 0.3,
        stream: true,
    });

    const stream = new ReadableStream({
        async start(controller) {
            for await (const chunk of res) {
                if (chunk.choices[0].delta.content) {
                    console.log('chunk delta ', chunk.choices[0].delta.content);
                    controller.enqueue(chunk.choices[0].delta.content);
                } else if (chunk.choices[0].finish_reason != null) {
                    controller.close();
                    break;
                }
            }
        },
    });
    return new Response(stream);
}
```

## 4 Bun 处理 CORS

非常简单：

```ts
const response = new Response(stream);
response.headers.set('Access-Control-Allow-Origin', '*');
response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
return response;
```

## 5 Bun 直接通过 HTTP 请求 gpt-4o

如果您不想引入 `openai` 依赖，您可以直接通过 HTTP 请求 gpt-4o。您只需进行一些额外的编码和解码，您需要一个新的依赖：`eventsource-parser`

```ts
import { createParser, type ParsedEvent, type ReconnectInterval } from 'eventsource-parser';

const apiKey = process.env.OPENAI_API_KEY;
const host = process.env.OPEN_AI_URL || 'api.openai.com';
const model = 'gpt-4o';

export default async function chatHttpImageIdea(req: Request) {
    const formData = await req.formData();
    const number = formData.get('number');
    const question = formData.get('question');
    const language = formData.get('language') as string;
    const file = formData.get('image') as File;

    const ab = await file.arrayBuffer();
    const baseImage = Buffer.from(ab).toString('base64');

    const prompt = `xxx`;

    const res = await fetch(`https://${host}/v1/chat/completions`, {
        headers: {
            'Content-Type': 'application/json',
            'Authorization': `Bearer ${apiKey}`,
        },
        method: 'POST',
        body: JSON.stringify({
            model: model,
            messages: [
                {
                    role: 'system',
                    content: prompt,
                },
                {
                    role: 'user',
                    content: [
                        {
                            type: 'image_url',
                            image_url: {
                                url: `data:image/jpeg;base64,${baseImage}`,
                            },
                        },
                    ],
                },
            ],
            temperature: 0.3,
            stream: true,
        }),
    });

    console.log('res', res);

    let counter = 0;
    const encoder = new TextEncoder();
    const decoder = new TextDecoder();

    const stream = new ReadableStream({
        async start(controller) {
            function onParse(event: ParsedEvent | ReconnectInterval) {
                if (event.type === 'event') {
                    const data = event.data;
                    if (data === '[DONE]') {
                        controller.close();
                        return;
                    }
                    try {
                        const json = JSON.parse(data);
                        const text = json.choices[0].delta?.content || '';
                        if (counter < 1 && (text.match(/\n/) || []).length) {
                            // 这是一个前缀字符（即 "\n\n"），什么也不做
                            return;
                        }
                        const queue = encoder.encode(text);
                        controller.enqueue(queue);
                        counter++;
                    } catch (e) {
                        controller.error(e);
                    }
                }
            }

            const parser = createParser(onParse);
            for await (const chunk of res.body as any) {
                console.log('chunk', chunk);
                parser.feed(decoder.decode(chunk));
            }
        },
    });

    const response = new Response(stream);
    response.headers.set('Access-Control-Allow-Origin', '*');
    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    return response;
}
```

希望这篇文章对你有用。我会继续分享下去。